﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Web;

namespace KWatkins.FluentMoqBuilder
{
	/// <summary>
	/// Implements many mock functions from <see cref="HttpRequestBase" />.
	/// </summary>
	/// <remarks>
	/// This approach, i.e. mocking everything with Moq rather than mocking via a custom subclass,
	/// is used because we still want to use a Moq in the builder so people can still add their own
	/// expectations via Mock.Get. Could we not create a subclass with a partial implementation and
	/// mock that instead? No because by default base implementations would not be called. We could
	/// set CallBase to true on the mock but then we'd have to mock pretty much everything in our
	/// subclass or we'd get lots of NotImplementedExceptions from <see cref="HttpRequestBase" />.
	/// This approach lets us just mock the things we want to mock using Moq and not have to worry
	/// about anything else misbehaving.
	/// </remarks>
	internal sealed class HttpRequestBaseBuilder : MockBuilder<HttpRequestBase>, IHttpRequestBaseBuilder
	{
		#region Instance Constructors
		internal HttpRequestBaseBuilder()
			: base(false)
		{
			// Static, default values.
			Mock.Setup(r => r.HttpMethod).Returns("GET");
			Mock.Setup(r => r.ApplicationPath).Returns("/");
			Mock.Setup(r => r.PhysicalApplicationPath).Returns(@"c:\inetpub\wwwroot\");
			Mock.Setup(r => r.Url).Returns(new Uri("http://localhost/"));
			// Functions values.
			Mock.Setup(r => r.AppRelativeCurrentExecutionFilePath).Returns(() => AppRelativeCurrentExecutionFilePath);
			Mock.Setup(r => r.CurrentExecutionFilePath).Returns(() => CurrentExecutionFilePath);
			Mock.Setup(r => r.FilePath).Returns(() => FilePath);
			Mock.Setup(r => r.Path).Returns(() => Path);
			Mock.Setup(r => r.PathInfo).Returns(() => PathInfo);
			Mock.Setup(r => r.RawUrl).Returns(() => RawUrl);
			Mock.Setup(r => r.QueryString).Returns(() => QueryString);
		}
		#endregion

		#region AppRelativeCurrentExecutionFilePath
		private string AppRelativeCurrentExecutionFilePath
		{
			get
			{
				if (Object.FilePath.StartsWith(Object.ApplicationPath))
				{
					return string.Concat("~/", Object.FilePath.Substring(Object.ApplicationPath.Length));
				}
				return string.Concat("~", Object.FilePath);
			}
		}
		#endregion

		#region CurrentExecutionFilePath
		private string CurrentExecutionFilePath
		{
			get
			{
				return RemoveTrailingSlashFromPathIfNotRoot(Object.Url.LocalPath);
			}
		}
		#endregion

		#region FilePath
		private string FilePath
		{
			get
			{
				return RemoveTrailingSlashFromPathIfNotRoot(Object.Url.LocalPath);
			}
		}
		#endregion

		#region Path
		private string Path
		{
			get
			{
				return Object.Url.LocalPath;
			}
		}
		#endregion

		#region PathInfo
		private string PathInfo
		{
			get
			{
				if (PathEndsWithSlashAndIsNotRoot(Object.Url.LocalPath))
				{
					return "/";
				}
				return "";
			}
		}
		#endregion

		#region Path Functions
		private static bool PathEndsWithSlashAndIsNotRoot(string path)
		{
			return !string.IsNullOrEmpty(path) && (path.Length > 1) && path.EndsWith("/");
		}
		private static string RemoveTrailingSlashFromPathIfNotRoot(string path)
		{
			if (PathEndsWithSlashAndIsNotRoot(path))
			{
				return path.Substring(0, path.Length - 1);
			}
			return path;
		}
		#endregion

		#region RawUrl
		private string RawUrl
		{
			get
			{
				return Object.Url.PathAndQuery;
			}
		}
		#endregion

		#region QueryString
		// Stores the original and parsed query string. We use the original to compare
		// against Url.Query; if someone changes it, e.g. using Mock.Get, then we can
		// reparse.
		private KeyValuePair<string, NameValueCollection>? queryString;

		private NameValueCollection QueryString
		{
			get
			{
				if ((queryString == null) || (Object.Url.Query != queryString.Value.Key))
				{
					queryString = new KeyValuePair<string, NameValueCollection>(
						Object.Url.Query,
						new ReadOnlyNameValueCollection(ParseQueryString(Object.Url.Query)));
				}
				return queryString.Value.Value;
			}
		}

		private static NameValueCollection ParseQueryString(string query)
		{
			if (string.IsNullOrEmpty(query) || (query.Length == 1))
			{
				return new NameValueCollection();
			}

			// Remove leading ?.
			query = query.Substring(1);

			var queryString = new NameValueCollection();

			int length = query.Length;
			for (int i = 0; i < length; i++)
			{
				int startIndex = i;
				int index = -1;
				while (i < length)
				{
					char ch = query[i];
					if (ch == '=')
					{
						if (index < 0)
						{
							index = i;
						}
					}
					else if (ch == '&')
					{
						break;
					}
					i++;
				}
				string key = null;
				string value;
				if (index >= 0)
				{
					key = query.Substring(startIndex, index - startIndex);
					value = query.Substring(index + 1, (i - index) - 1);
				}
				else
				{
					value = query.Substring(startIndex, i - startIndex);
				}
				queryString.Add(HttpUtility.UrlDecode(key), HttpUtility.UrlDecode(value));

				if ((i == (index - 1)) && (query[i] == '&'))
				{
					queryString.Add(null, string.Empty);
				}
			}
			return queryString;
		}
		#endregion

		#region IHttpRequestBaseBuilder Members
		public IHttpRequestBaseBuilder WithUrl(string url)
		{
			Validate.Argument(url, "url")
				.IsNotNull()
				.IsNotEmpty();

			return WithUrl(new Uri(url));
		}

		public IHttpRequestBaseBuilder WithUrl(Uri url)
		{
			Validate.Argument(url, "url")
				.IsNotNull();

			Mock
				.Setup(r => r.Url)
				.Returns(url);

			return this;
		}

		public IHttpRequestBaseBuilder WithUrlReferrer(string urlReferrer)
		{
			Validate.Argument(urlReferrer, "urlReferrer")
				.IsNotNull()
				.IsNotEmpty();

			return WithUrlReferrer(new Uri(urlReferrer));
		}

		public IHttpRequestBaseBuilder WithUrlReferrer(Uri urlReferrer)
		{
			Validate.Argument(urlReferrer, "urlReferrer")
				.IsNotNull();

			Mock.Setup(r => r.UrlReferrer).Returns(urlReferrer);
			return this;
		}

		public IHttpRequestBaseBuilder WithApplicationPath(string applicationPath)
		{
			Validate.Argument(applicationPath, "applicationPath")
				.IsNotNull()
				.IsNotEmpty();

			Mock
				.Setup(r => r.ApplicationPath)
				.Returns(applicationPath);

			return this;
		}

		public IHttpRequestBaseBuilder WithPhysicalApplicationPath(string physicalApplicationPath)
		{
			Validate.Argument(physicalApplicationPath, "physicalApplicationPath")
				.IsNotNull()
				.IsNotEmpty();

			Mock
				.Setup(r => r.PhysicalApplicationPath)
				.Returns(physicalApplicationPath);

			return this;
		}
		#endregion
	}
}
